/*
 *  Portal Check - Determines if a user is in an IP restrict pool or behind a
 *                 captive portal.
 *
 *  This file initially created by Google, Inc.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <stdlib.h>
#include <time.h>

#include <curl/curl.h>

#include "connman.h"

#define lengthof(array) (sizeof (array) / sizeof ((array)[0]))
#define	_DBG_PORTAL(fmt, arg...)	DBG(DBG_PORTAL, fmt, ## arg)

#define MSEC_PER_SEC 1000LL
#define NSEC_PER_SEC 1000000000LL

#define REQUEST_TIMEOUT_S 10

static CURLM *curl_multi_handle_;

struct curl_source {
	GSource source; /* base class  */
	guint fd_bmap;
	GPollFD poll_fds[sizeof(guint) * 8];
	struct timespec expiration;
};

static struct curl_source *curl_source;

struct portal_request {
	struct connman_service *service;
	struct connman_device *device;
	char *interface;
	CURL *easy_handle;
	connman_bool_t handle_added;
};

static GSList *request_list = NULL;

/**
 * Allocate a new portal request and make it the latest request
 */
static struct portal_request *new_request(struct connman_service *service,
					  struct connman_device *device,
					  const char *interface,
					  CURL *easy_handle)
{
	struct portal_request *request = g_try_new0(struct portal_request, 1);
	if (request == NULL)
		return NULL;
	request->service = connman_service_ref(service);
	request->device = connman_device_ref(device);
	request->interface = g_strdup(interface);
	request->easy_handle = easy_handle;
	request->handle_added = FALSE;

	connman_device_rp_filter_disable(device);

	request_list = g_slist_append(request_list, request);

	return request;
}

/**
 * Free a portal request
 */
static void free_request(struct portal_request *request)
{
	request_list = g_slist_remove(request_list, request);

	if (request->handle_added)
		curl_multi_remove_handle(curl_multi_handle_,
					 request->easy_handle);
	curl_easy_cleanup(request->easy_handle);
	connman_device_rp_filter_enable(request->device);
	g_free(request->interface);
	connman_device_unref(request->device);
	connman_service_unref(request->service);
	g_free(request);
}

/**
 * Cancels a request
 */
static void cancel_request(struct portal_request *request)
{
	_DBG_PORTAL("%s: cancelling old request", request->interface);
	free_request(request);
}

/**
 * Cancels old requests on this request's interface
 */
static void cancel_old_requests(struct portal_request *request)
{
	GSList *list;

	list = request_list;
	while(list != NULL)  {
		struct portal_request *old_request = list->data;

		list = list->next;
		if (old_request == request)
			continue;

		if (request->service == old_request->service)
			cancel_request(old_request);
	}
}

/**
 * Cancels old requests on this service
 */
static void cancel_service_requests(struct connman_service *service)
{
	GSList *list;

	for (list = request_list; list != NULL; list = list->next) {
		struct portal_request *request = list->data;

		if (request->service == service)
			cancel_request(request);
	}
}

/**
 * Return TRUE if there is a request in progress for this service.
 */
static connman_bool_t service_has_request(struct connman_service *service)
{
	GSList *list;

	for (list = request_list; list != NULL; list = list->next) {
		struct portal_request *request = list->data;

		if (request->service == service)
			return TRUE;
	}
	return FALSE;
}

/**
 * Returns the HTTP response code from a completed curl request.
 */
static long get_http_code(CURL *curl_handle)
{
	long http_code = 0;
	curl_easy_getinfo (curl_handle, CURLINFO_RESPONSE_CODE, &http_code);
	_DBG_PORTAL("http response code is %d", (int) http_code);
	return http_code;
}

/**
 * Returns a connectivity state based on the status of an HTTP request
 * to portal check URL
 */
static enum connman_service_connectivity_state check_connectivity_state(
	CURLMsg *request_status,
	const char *interface)
{
	CURLcode code = (CURLcode)request_status->data.result;

	/* Check the request state */
	if (CURLE_COULDNT_RESOLVE_HOST == code) {
		/*
		 * TODO(jglasgow): test resolving the address of the
		 * captive portal web server instead
		 */
		connman_info("%s: cannot resolve %s, marking portal",
		    interface,
		    __connman_profile_get_portal_url());
		return CONNMAN_SERVICE_CONNECTIVITY_STATE_NONE;
	}

	if (CURLE_OK == code) {
		int http_status_code = get_http_code(request_status->easy_handle);
		if (204 == http_status_code) {
			connman_info("%s: interface can route traffic, marking online",
				     interface);
			return CONNMAN_SERVICE_CONNECTIVITY_STATE_UNRESTRICTED;
		}
		connman_info("%s: http return code %d, marking portal",
			     interface, http_status_code);
		return CONNMAN_SERVICE_CONNECTIVITY_STATE_RESTRICTED;
	}
	connman_info("%s: http_get_failed, curl code %d, marking portal",
		     interface, code);
	return CONNMAN_SERVICE_CONNECTIVITY_STATE_RESTRICTED;
}

/**
 * Check the result of previous HTTP requests.
 */
static void check_request_status(void)
{
	int remaining_updates;
	CURLMsg* request_status;
	struct portal_request *portal_request = NULL;
	struct connman_service *service;

	while ((request_status =
		curl_multi_info_read(curl_multi_handle_, &remaining_updates))) {

		if (CURLMSG_DONE != request_status->msg)
			continue;

		curl_easy_getinfo(request_status->easy_handle, CURLINFO_PRIVATE,
				  (char **)&portal_request);
		service = portal_request->service;

		if (service != NULL) {
			enum connman_service_connectivity_state state =
				check_connectivity_state(request_status,
					portal_request->interface);

			connman_service_set_connectivity_state(service, state);

		} else {
			_DBG_PORTAL("%s: Ignoring stale results",
				    portal_request->interface);
		}
		free_request(portal_request);
	}
}

/**
 * Check the timeout set by libcurl has expired.
 *
 * Returns TRUE if it has expired.
 */
static inline gboolean source_check_expiration(GSource* source, gint *timeout)
{
	struct curl_source *src = (struct curl_source *)source;
	struct timespec now;
	gint delay_ms;

	if (src->expiration.tv_sec == LONG_MAX) {
		if (timeout)
			*timeout = -1;
		return FALSE;
	}

	clock_gettime(CLOCK_MONOTONIC, &now);

	delay_ms = (src->expiration.tv_sec - now.tv_sec) * MSEC_PER_SEC
		 + src->expiration.tv_nsec / (NSEC_PER_SEC / MSEC_PER_SEC)
		 - now.tv_nsec / (NSEC_PER_SEC / MSEC_PER_SEC);

	if (delay_ms <= 0) {
		_DBG_PORTAL("request has timeout'ed");
		return TRUE;
	} else {
		if (timeout)
			*timeout = delay_ms;
		return FALSE;
	}
}

/**
 * Callback triggered before the main loop select operation.
 */
static gboolean source_prepare(GSource* source, gint* timeout)
{
	return source_check_expiration(source, timeout);
}

/**
 * Callback triggered when exiting the main loop select.
 */
static gboolean source_check(GSource* source)
{
	int i, bmap;
	struct curl_source *src = (struct curl_source *)source;

	if (source_check_expiration(source, NULL))
		return TRUE;

	bmap = src->fd_bmap;
	while ((i = __builtin_ffs(bmap) - 1) >= 0) {
		if (src->poll_fds[i].revents)
			return TRUE;

		bmap &= ~(1 << i);
	}

	return FALSE;
}

/**
 * Conversion table between glib poll events and libcurl select events.
 */
static struct {
	int curl_event;
	int poll_mask;
} event_flags[] = {
	{ CURL_CSELECT_IN, G_IO_IN | G_IO_PRI },
	{ CURL_CSELECT_OUT, G_IO_OUT },
	{ CURL_CSELECT_ERR, G_IO_ERR | G_IO_HUP | G_IO_NVAL },
};

/**
 * Process the request if there is an event of the file descriptors.
 */
static gboolean source_dispatch(GSource *source, GSourceFunc callback,
				gpointer user_data)
{
	int i, e, bmap;
	int remaining = -1;
	struct curl_source *src = (struct curl_source *)source;
	CURLMcode mcode;

	if (source_check_expiration(source, NULL)) {
		mcode = curl_multi_perform(curl_multi_handle_, &remaining);
		_DBG_PORTAL("mcode = %d, remaining = %d", mcode, remaining);
	}

	bmap = src->fd_bmap;
	while ((i = __builtin_ffs(bmap) - 1) >= 0) {
		if (src->poll_fds[i].revents) {
			int ev_bitmask = 0;

			for (e = 0; e < G_N_ELEMENTS(event_flags); e++)
				if (src->poll_fds[i].revents &
					event_flags[e].poll_mask)
					ev_bitmask |= event_flags[e].curl_event;

			mcode = curl_multi_socket_action(curl_multi_handle_,
						 src->poll_fds[i].fd,
						 ev_bitmask,
						 &remaining);

			_DBG_PORTAL("mcode = %d, remaining = %d", mcode, remaining);
		}
		bmap &= ~(1 << i);
	}

	if (!remaining) {
		/* no more pending transfer, set infinite timeout */
		curl_source->expiration.tv_sec = LONG_MAX;
	}
	check_request_status();

	return TRUE;
}

/**
 * Define the source used to poll on the request descriptors.
 */
static GSourceFuncs source_callbacks = {
	source_prepare, source_check, source_dispatch, 0, 0, 0
};

/**
 * Update the file descriptors used to monitor in the main loop.
 */
static int socket_callback(CURL *easy, curl_socket_t s, int action,
			   void *userp, void *socketp)
{
	gushort events = G_IO_ERR | G_IO_HUP;
	struct curl_source *src = userp;
	GPollFD *poll_fd = socketp;
	int idx;

	switch (action) {
	case CURL_POLL_IN:
	case CURL_POLL_OUT:
	case CURL_POLL_INOUT:
		if (socketp) {
			g_source_remove_poll(&src->source, poll_fd);
		} else {
			if ((idx = (__builtin_ffs(~src->fd_bmap) - 1)) < 0) {
				_DBG_PORTAL("Cannot set FD, no empty slot.\n");
				break;
			}
			src->fd_bmap |= (1 << idx);
			_DBG_PORTAL("FD#%d set (using slot %d)", s, idx);
			poll_fd = &src->poll_fds[idx];
			poll_fd->fd = s;
			curl_multi_assign(curl_multi_handle_, s, poll_fd);
		}
		if ((action == CURL_POLL_IN) || (action == CURL_POLL_INOUT))
			events |= G_IO_IN | G_IO_PRI;
		if ((action == CURL_POLL_OUT) || (action == CURL_POLL_INOUT))
			events |= G_IO_OUT;
		poll_fd->events = events;
		poll_fd->revents = 0;
		g_source_add_poll(&src->source, poll_fd);
		break;
	case CURL_POLL_REMOVE:
		if (poll_fd) {
			poll_fd->revents = 0;
			g_source_remove_poll(&src->source, poll_fd);
			_DBG_PORTAL("FD#%d unset (using slot %d)",
				    poll_fd->fd,
				    (int) (poll_fd - src->poll_fds));
			src->fd_bmap &= ~(1 << (poll_fd - src->poll_fds));
			curl_multi_assign(curl_multi_handle_, s, NULL);
		}
		break;
	}
	return 0;
}

/**
 * Compute and record the timeout requested by libcurl.
 */
static int timer_callback(CURLM *multi, long timeout_ms, void *userp)
{
	struct curl_source *src = userp;

	clock_gettime(CLOCK_MONOTONIC, &src->expiration);

	if (timeout_ms >= 0) {
		src->expiration.tv_sec +=
			((long long)timeout_ms * 1000000LL) / NSEC_PER_SEC;
		src->expiration.tv_nsec +=
			((long long)timeout_ms * 1000000LL) % NSEC_PER_SEC;
		if (src->expiration.tv_nsec >= NSEC_PER_SEC)
		{
			src->expiration.tv_sec++;
			src->expiration.tv_nsec -= NSEC_PER_SEC;
		}

	} else {
		src->expiration.tv_sec = LONG_MAX;
	}
	_DBG_PORTAL("set timeout %ld ms (expires %ld.%09ld)", timeout_ms,
		    src->expiration.tv_sec, src->expiration.tv_nsec);
	return 0;
}

/**
 * Returns TRUE if a service has entered a state where it may go away soon
 */
static gboolean should_cancel_request(struct connman_service *service)
{
	const char *service_state;

	if (service == NULL) {
		_DBG_PORTAL("SKIP, NULL service");
		return FALSE;
	}
	service_state = connman_service_get_state(service);
	if ((g_strcmp0(service_state, "idle") == 0) ||
	    (g_strcmp0(service_state, "disconnect") == 0) ||
	    (g_strcmp0(service_state, "failure") == 0) ||
	    (g_strcmp0(service_state, "activation-failure") == 0))
	{
		_DBG_PORTAL("CANCEL, service %s state %s",
			    connman_service_get_identifier(service), service_state);
		return TRUE;
	}
	return FALSE;
}

/**
 * Returns TRUE if the new default route is "ready"
 */
static gboolean should_process_request(struct connman_service *service)
{
	const char *service_state;

	if (service == NULL) {
		_DBG_PORTAL("SKIP, NULL service");
		return FALSE;
	}
	service_state = connman_service_get_state(service);
	if (g_strcmp0(service_state, "ready") != 0) {
		_DBG_PORTAL("SKIP, service %s state %s != ready",
		    connman_service_get_identifier(service), service_state);
		return FALSE;
	}
	return TRUE;
}

/**
 * Unref a service on the idle thread.
 */
static gboolean __unref_service(gpointer data)
{
	struct connman_service *service = data;
	const char *interface = connman_service_get_interface(service);

	_DBG_PORTAL("%s: unref service",
		    interface != NULL ? interface : "unknown");

	connman_service_unref(service);
	return FALSE;
}

/**
 * Cycle a service to the ONLINE state.  Used when portal check
 * is disabled for the service (called from the idle loop).
 */
static gboolean __mark_unrestricted(gpointer data)
{
	struct connman_service *service = data;
	const char *interface = connman_service_get_interface(service);

	_DBG_PORTAL("%s: BYPASS, mark service ONLINE",
		    interface != NULL ? interface : "unknown");

	connman_service_set_connectivity_state(service,
	    CONNMAN_SERVICE_CONNECTIVITY_STATE_UNRESTRICTED);
	connman_service_unref(service);
	return FALSE;
}

static int debug_function(CURL *handle, curl_infotype type,
			  char *data, size_t size, void *userptr)
{
	if (type == CURLINFO_TEXT)
		_DBG_PORTAL("curl: %s", data);
	return 0;
}

static void __portal_test(struct connman_service *service)
{
	struct connman_device *device;
	const char *interface;
	const struct connman_resolver_state *resolver_state;
	CURLcode ecode;
	CURLMcode mcode;
	CURL * request_handle;
	struct portal_request *portal_request;
	connman_bool_t first = TRUE;
	GString *servers = NULL;
	char **p;
	static const struct {
		CURLoption option;
		unsigned int parameter;
		const char *option_name;
	} options[] = {
		/* Do not let curl cache DNS entries */
		{CURLOPT_DNS_CACHE_TIMEOUT, 0, "DNS_CACHE_TIMEOUT"},

		/* Do not allow curl to reuse the connection */
		{CURLOPT_FORBID_REUSE, 1, "FORBID_REUSE"},

		/* Set a timeout for the request in seconds */
		{CURLOPT_TIMEOUT, REQUEST_TIMEOUT_S, "TIMEOUT"},

		/* Set a timeout for the TCP/IP connect in seconds */
		{CURLOPT_CONNECTTIMEOUT, REQUEST_TIMEOUT_S, "CONNECTTIMEOUT"},

		/* Ensure CURL does not send signals or install handlers */
		{CURLOPT_NOSIGNAL, 1, "NOSIGNAL"},

		/* Instruct curl to generate debugging information */
		{CURLOPT_VERBOSE, 1, "VERBOSE"},
	};
	int i;

	if (__connman_service_check_portal(service) == FALSE) {
		/*
		 * The service or device is not meant to do a portal
		 * check; mark it online immediately.
		 */
		g_idle_add(__mark_unrestricted, connman_service_ref(service));
		return;
	}

	device = connman_service_get_device(service);
	if (device == NULL) {
		connman_error("%s: no device for service %p",
			__func__, service);
		return;
	}
	interface = connman_device_get_interface(device);
	if (interface == NULL) {
		connman_error("%s: no interface for device %p (service %p)",
			__func__, device, service);
		return;
	}
	resolver_state = connman_resolver_lookup(interface);
	if (resolver_state == NULL) {
		connman_error("%s: no resolver state for service %p",
			__func__, service);
		return;
	}

	_DBG_PORTAL("%s: Checking %s on %s",
		interface,
		__connman_profile_get_portal_url(),
		connman_service_get_identifier(service));

	request_handle = curl_easy_init();
	if (request_handle == NULL) {
		connman_error("%s: easy request is NULL", __func__);
		return;
	}

	/* The portal request will take ownership of the request_handle */
	portal_request = new_request(service, device, interface, request_handle);
	if (portal_request == NULL) {
		connman_error("%s: Cannot allocate portal request.", __func__);
		curl_easy_cleanup(request_handle);
		return;
	}

	/* Set standard options */
	for (i = 0; i < lengthof(options); i++) {
		ecode = curl_easy_setopt(request_handle,
					 options[i].option,
					 options[i].parameter);
		if (ecode != CURLE_OK) {
			connman_error("%s :%s = %s", __func__,
				      options[i].option_name,
				      curl_easy_strerror(ecode));
			goto error;
		}
	}

	/* Set pointer options */
	ecode = curl_easy_setopt(request_handle,
				 CURLOPT_PRIVATE, portal_request);
	if (ecode != CURLE_OK) {
		connman_error("%s: Unable to set PRIVATE: %s", __func__,
		    curl_easy_strerror(ecode));
		goto error;
	}
	ecode = curl_easy_setopt(request_handle, CURLOPT_URL,
		 __connman_profile_get_portal_url());
	if (ecode != CURLE_OK) {
		connman_error("%s: Unable to set URL: %s", __func__,
		    curl_easy_strerror(ecode));
		goto error;
	}
	ecode = curl_easy_setopt(request_handle, CURLOPT_INTERFACE, interface);
	if (ecode != CURLE_OK) {
		connman_error("%s: Unable to set INTERFACE: %s", __func__,
		    curl_easy_strerror(ecode));
		goto error;
	}

	/* name servers */
	servers = g_string_sized_new(128);
	for(p = resolver_state->servers; *p != NULL; p++) {
		if (!first)
			g_string_append_c(servers, ',');
		g_string_append(servers, *p);
		first = FALSE;
	}
	_DBG_PORTAL("%s: Using name servers %s", interface, servers->str);

	ecode = curl_easy_setopt(request_handle,
				 CURLOPT_DNS_SERVERS, servers->str);
	if (ecode != CURLE_OK) {
		connman_error("%s: Unable to set DNS_SERVERS: %s",
		    __func__, curl_easy_strerror(ecode));
		goto error;
	}


	ecode = curl_easy_setopt(request_handle,
				 CURLOPT_DEBUGFUNCTION, debug_function);
	if (ecode != CURLE_OK) {
		connman_error("%s: Unable to set DEBUGFUNCTION: %s",
		    __func__, curl_easy_strerror(ecode));
		goto error;
	}

	mcode = curl_multi_add_handle(curl_multi_handle_, request_handle);
	if (mcode != CURLM_OK) {
		connman_error("%s: curl_multi_add_handle() = %s", __func__,
			      curl_multi_strerror(mcode));
		goto error;
	}
	portal_request->handle_added = TRUE;

	cancel_old_requests(portal_request);

	curl_multi_socket_action(curl_multi_handle_, CURL_SOCKET_BAD, 0, &i);
	g_string_free(servers, TRUE);
	return;

error:
	g_string_free(servers, TRUE);
	free_request(portal_request);
}

/**
 * Check to see if we're in an IP restrict pool everytime a service
 * becomes ready.
 */
static void portal_service_state_changed(struct connman_service *service)
{
	if (should_cancel_request(service)) {
		/*
		 * cancel_service_requests might unref the last
		 * reference to the service.  That would free the
		 * service object and mean that callers higher up the
		 * stack might dereference an invalid service object.
		 * Prevent that by adding a reference here, and then
		 * releasing that reference on the idle thread.
		 */
		connman_service_ref(service);
		cancel_service_requests(service);
		g_idle_add(__unref_service, service);

		return;
	}

	if (!should_process_request(service))
		return;

	__portal_test(service);
}

/**
 * recheck a service to see if it is in the portal state or online
 */
void __connman_portal_service_recheck_state(struct connman_service *service)
{
	if (service_has_request(service))
		return;

	__portal_test(service);
}

static struct connman_notifier portal_notifier = {
	.name			= "portal",
	.priority		= CONNMAN_NOTIFIER_PRIORITY_LOW,
	.service_state_changed  = portal_service_state_changed,
};

int __connman_portal_init(void)
{
	CURLMcode mcode;

	if (connman_notifier_register(&portal_notifier) < 0) {
		connman_error("Failed to register the portal notifier");
		return -1;
	}

	curl_multi_handle_ = curl_multi_init();
	if (!curl_multi_handle_) {
		_DBG_PORTAL("Unable to initialize libcurl");
		connman_notifier_unregister(&portal_notifier);
		return -1;
	}

	curl_source = (struct curl_source *)
		g_source_new(&source_callbacks, sizeof(struct curl_source));
	curl_source->fd_bmap = 0;
	curl_source->expiration.tv_sec = LONG_MAX;
	mcode = curl_multi_setopt(curl_multi_handle_, CURLMOPT_SOCKETFUNCTION,
				  socket_callback);
	if (mcode != CURLM_OK) {
		connman_error("%s: curl_multi_setopt(SOCKETFUNCTION) = %s",
			      __func__, curl_multi_strerror(mcode));
		goto err_source;
	}
	mcode = curl_multi_setopt(curl_multi_handle_, CURLMOPT_SOCKETDATA,
				  curl_source);
	if (mcode != CURLM_OK) {
		connman_error("%s: curl_multi_setopt(SOCKETDATA) = %s",
			      __func__, curl_multi_strerror(mcode));
		goto err_source;
	}
	mcode = curl_multi_setopt(curl_multi_handle_, CURLMOPT_TIMERFUNCTION,
				  timer_callback);
	if (mcode != CURLM_OK) {
		connman_error("%s: curl_multi_setopt(TIMERFUNCTION) = %s",
			      __func__, curl_multi_strerror(mcode));
		goto err_source;
	}
	mcode = curl_multi_setopt(curl_multi_handle_, CURLMOPT_TIMERDATA,
				  curl_source);
	if (mcode != CURLM_OK) {
		connman_error("%s: curl_multi_setopt(TIMERDATA) = %s",
			      __func__, curl_multi_strerror(mcode));
		goto err_source;
	}
	g_source_attach(&curl_source->source, NULL);

	return 0;

err_source:
	g_source_unref(&curl_source->source);
	curl_multi_cleanup(curl_multi_handle_);
	connman_notifier_unregister(&portal_notifier);
	return -1;
}

void __connman_portal_cleanup(void)
{
	connman_notifier_unregister(&portal_notifier);
	g_source_destroy(&curl_source->source);
	g_source_unref(&curl_source->source);
	curl_multi_cleanup(curl_multi_handle_);
}
